/*
 * Copyright 2024 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.pajamas.starter.integration.module.client;

import com.google.common.collect.Sets;

import org.apache.commons.lang3.StringUtils;
import org.pajamas.starter.core.util.SequenceUtils;
import org.pajamas.starter.integration.spring.cloud.RouteDefinitionBuilder;
import org.pajamas.starter.integration.spring.cloud.RouteDefinitionUtils;
import org.pajamas.starter.integration.spring.event.reactive.FluxPropertyChangedEventHandler;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.cloud.gateway.event.RefreshRoutesEvent;
import org.springframework.cloud.gateway.filter.factory.StripPrefixGatewayFilterFactory;
import org.springframework.cloud.gateway.handler.predicate.PathRoutePredicateFactory;
import org.springframework.cloud.gateway.route.RouteDefinitionRepository;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.core.env.PropertyResolver;

import java.net.URISyntaxException;
import java.util.Arrays;
import java.util.Iterator;
import java.util.Objects;
import java.util.Set;
import java.util.StringJoiner;

import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

/**
 * @author william
 * @since 2024/3/30
 */
@ConditionalOnProperty(prefix = "org.pajamas.client.gateway.routes.building", value = "register", havingValue = "true")
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)
public class PajamasRouteDefinitionRegistrar extends FluxPropertyChangedEventHandler<String, String> {
    private static final String ROUTE_PATH_SUFFIX = "/**";

    private final PropertyResolver propertyResolver;
    private final RouteDefinitionRepository repository;
    private final ApplicationContext applicationContext;
    private final ApplicationEventPublisher eventPublisher;
    private final Iterator<RouteDefinitionBuilderCustomizer> routeDefinitionBuilderCustomizers;

    @Value("${server.servlet.context-path:}")
    private String contextPath;

    protected PajamasRouteDefinitionRegistrar(PropertyResolver propertyResolver, RouteDefinitionRepository repository
            , ApplicationContext applicationContext, ApplicationEventPublisher eventPublisher
            , ObjectProvider<RouteDefinitionBuilderCustomizer> routeDefinitionBuilderCustomizers) {
        super("org.pajamas.client.gateway.routes.dynamic.serviceNames", false);
        this.propertyResolver = propertyResolver;
        this.repository = repository;
        this.applicationContext = applicationContext;
        this.eventPublisher = eventPublisher;
        this.routeDefinitionBuilderCustomizers = routeDefinitionBuilderCustomizers.orderedStream().iterator();
    }

    @Override
    protected Flux<String> getUnchangedProperty() {
        return this.repository
                .getRouteDefinitions()
                .filter(Objects::nonNull)
                .mapNotNull(definition -> {
                    String id = definition.getId();
                    String normalizedId = SequenceUtils.normalize(id);
                    return RouteDefinitionUtils.routeDefinitionId2ServiceName(normalizedId);
                });
    }

    @SuppressWarnings("unchecked")
    @Override
    protected Flux<String> getChangedProperty() {
        return Flux
                .fromIterable((Set<String>) this.propertyResolver.getProperty(this.getPropertyKey(), Set.class,
                        Sets.newHashSet()))
                .map(String.class::cast)
                .filter(StringUtils::isNotBlank)
                .map(SequenceUtils::normalize);
    }

    @Override
    protected String keyExtractor(String property) {
        return property;
    }

    @Override
    protected Mono<Void> increase(Mono<String> mono) {
        return mono
                .map(serviceName -> {
                    try {
                        return RouteDefinitionBuilder
                                .newBuilder()
                                .id(RouteDefinitionUtils.serviceName2RouteDefinitionId(serviceName))
                                .lbUri(serviceName)
                                .addPredicate(PathRoutePredicateFactory.class, this.normalizeRoutePath(serviceName))
                                .addFilter(StripPrefixGatewayFilterFactory.class, String.valueOf(2))
                                .customizers(this.routeDefinitionBuilderCustomizers)
                                .build();
                    } catch (URISyntaxException ex) {
                        throw new PajamasGatewayUriRebuildException(ex);
                    }
                })
                .onErrorResume(PajamasGatewayUriRebuildException.class, Mono::error)
                .map(definition -> this.repository.save(Mono.justOrEmpty(definition)))
                .flatMap(ref -> ref);

    }

    @Override
    protected Mono<Void> decrease(Mono<String> mono) {
        return mono
                .map(RouteDefinitionUtils::serviceName2RouteDefinitionId)
                .map(id -> this.repository.delete(Mono.justOrEmpty(id)))
                .flatMap(ref -> ref);
    }

    @Override
    protected void onSuccess() {
        this.eventPublisher.publishEvent(new RefreshRoutesEvent(this.applicationContext));
    }

    private String normalizeRoutePath(String serviceName) {
        StringJoiner joiner = new StringJoiner("/", "/", ROUTE_PATH_SUFFIX);
        Arrays.stream(SequenceUtils
                        .normalize(this.contextPath)
                        .split("/"))
                .filter(StringUtils::isNotBlank)
                .forEach(joiner::add);
        return joiner.add(serviceName).toString();
    }

    public interface RouteDefinitionBuilderCustomizer extends RouteDefinitionBuilder.Customizer {
    }
}
